package gov.nist.ssd.swqb.core;

import gov.nist.ssd.swqb.core.files.PrefixManager;
import com.hp.hpl.jena.query.*;
import com.hp.hpl.jena.query.larq.IndexLARQ;
import com.hp.hpl.jena.query.larq.LARQ;
import com.hp.hpl.jena.rdf.model.Literal;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.tdb.TDBFactory;
import gov.nist.ssd.swqb.core.exceptions.InvalidURIException;
import gov.nist.ssd.swqb.core.exceptions.OutOfBoundsException;
import gov.nist.ssd.swqb.core.exceptions.TimeoutException;
import gov.nist.ssd.swqb.events.ChangeDisplayCountEvent;
import gov.nist.ssd.swqb.events.ChangeIndexEvent;
import gov.nist.ssd.swqb.events.SetFocusEvent;
import gov.nist.ssd.swqb.listeners.ChangeDisplayCountListener;
import gov.nist.ssd.swqb.listeners.ChangeIndexListener;
import gov.nist.ssd.swqb.listeners.SetFocusListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.lucene.index.IndexReader;

import static gov.nist.ssd.swqb.config.DebugMode.*;
import static gov.nist.ssd.swqb.config.RequestsTimeout.*;

/**
 *
 * @author benjamin herbomez
 */
public class Display {

    private final Model model;
    private RDFNode currentNode;
    private int currentPage;
    private int displayNumber = 7;
    private int nodeCount = 0;
    private static PrefixManager prefixManager;
    protected List<EventListener> listeners = new ArrayList<EventListener>();

    public Display(String datasetPath, PrefixManager pfManager) throws IOException, InvalidURIException, TimeoutException {

        prefixManager = pfManager;

        File TDBFile = new File(datasetPath);
        if (!TDBFile.exists() || !TDBFile.isDirectory()) {
            throw new IOException(datasetPath + " is not a valid path");
        }
        //Load the TDB database in a Jena Model
        this.model = TDBFactory.createModel(TDBFile.getAbsolutePath());
        //Load the associated lucene index (to allow for 'pf:textMatch' predicates in sparql queries)
        LARQ.setDefaultIndex(new IndexLARQ(IndexReader.open(TDBFile.getPath() + ".index")));

        StmtIterator listStatements = model.listStatements();
        RDFNode uri = null;
        while (listStatements.hasNext()) {
            uri = listStatements.next().getSubject();
            if (uri != null) {
                break;
            }
        };
        this.setFocus(uri);
    }

    public Display(String datasetPath, PrefixManager prefixManager, int displayCount) throws IOException, InvalidURIException, TimeoutException {
        this(datasetPath, prefixManager);
        this.displayNumber = displayCount;
        try {
            this.setPageIndex(0);
        } catch (OutOfBoundsException ex) {
            Logger.getLogger(Display.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void setDisplayCount(int d) {
        this.displayNumber = d;
        while (this.getCurrentPageIndex() >= this.getPageCount()) {
            this.currentPage--;
        }
        ChangeDisplayCountEvent evt = new ChangeDisplayCountEvent(this, displayNumber);
        for (EventListener e : this.listeners) {
            if (e instanceof ChangeDisplayCountListener) {
                ((ChangeDisplayCountListener) e).displayCountChanged(evt);
            }
        }
    }

    public int getDisplayCount() {
        return this.displayNumber;
    }

    public final void setFocus(RDFNode uri) throws InvalidURIException, TimeoutException {
        this.currentNode = uri;
        this.currentPage = 0;

        SetFocusEvent evt = new SetFocusEvent(this,uri);
        if(this.listeners != null)
            for (EventListener e : this.listeners) {
                if (e instanceof SetFocusListener) {
                    ((SetFocusListener) e).setFocusStart(evt);
                }
            }


        //Count the neightbours
        
        String countQuery;

        if (uri.isLiteral()) {
            String r = Display.RDFNodeAsSparqlArg(this.currentNode);
            
            countQuery = prefixManager.getPrefixsTurtleFormat()
                    + "PREFIX pf: <http://jena.hpl.hp.com/ARQ/property#>\n"
                    + "SELECT (COUNT(*) AS ?count)\n"
                    + "WHERE {\n"
                    + "  ?x ?y " + r + " . \n"
                    + "} \n";
        } else {
            String r = Display.RDFNodeAsSparqlArg(this.currentNode);
            
            countQuery = prefixManager.getPrefixsTurtleFormat()
                    + "SELECT (COUNT(*) AS ?count)\n"
                    + "WHERE {\n"
                    + "     {\n"
                    + "         " + r + " ?y ?x .\n"
                    + "     }\n"
                    + "     UNION\n"
                    + "     {\n"
                    + "         ?x ?y " + r + " .\n"
                    + "     }\n"
                    + "}\n";
        }
        
        if (debugModeIsOn()) {
            printSparqlDebugInformations(
                    dClassName("gov.nist.ssd.swqb.core.Display"),
                    dMethodName("setFocus"),
                    dRequestGoal("count node neighbours"),
                    dSparqlText(countQuery));
        }

        Query cQuery = QueryFactory.create(countQuery, Syntax.syntaxARQ);
        QueryExecution cQueryExec = QueryExecutionFactory.create(cQuery, model);
        if (requestsTimeout() > 0) {
            cQueryExec.setTimeout(requestsTimeout());
        }
        ResultSet resultSet = cQueryExec.execSelect();
        try {
            if (resultSet.hasNext()) {
                QuerySolution next = resultSet.next();
                int count = next.getLiteral("count").getInt();
                this.nodeCount = count;
            }
        } catch (com.hp.hpl.jena.query.QueryCancelledException ex) {
            throw new TimeoutException();
        }

        for (EventListener e : this.listeners) {
            if (e instanceof SetFocusListener) {
                ((SetFocusListener) e).setFocusEnd(evt);
            }
        }
    }

    public RDFNode getCurrentFocus() {
        return this.currentNode;
    }

    public final void setPageIndex(int p) throws OutOfBoundsException {
        if (p < 0 || p > this.nodeCount) {
            throw new OutOfBoundsException();
        }
        this.currentPage = p;

        ChangeIndexEvent evt = new ChangeIndexEvent(model, p);
        for (EventListener e : this.listeners) {
            if (e instanceof ChangeIndexListener) {
                ((ChangeIndexListener) e).indexChanged(evt);
            }
        }
    }

    public int getCurrentPageIndex() {
        return this.currentPage;
    }

    public int getPageCount() {
        return (int) Math.ceil(new Double(this.nodeCount)/new Double(this.displayNumber));
    }

    public List<Statement> getPageContent() {
        List<Statement> ret = new ArrayList<Statement>();
        List<RDFNode> subList = this.getNodePageList();

        String prefixes = prefixManager.getPrefixsTurtleFormat();
        String toClause = getDisjonctiveFilterClause(subList);

        for (RDFNode n : subList) {

            String queryString1 =
                    getPageContentSparqlRequest1(prefixes, RDFNodeAsSparqlArg(n), toClause);

            String queryString2 =
                    getPageContentSparqlRequest2(prefixes, RDFNodeAsSparqlArg(n), toClause);

            if (debugModeIsOn()) {
                printSparqlDebugInformations(
                        dClassName("gov.nist.ssd.swqb.core.Display"),
                        dMethodName("getPageContent"),
                        dRequestGoal("search link between nodes"),
                        dSparqlText(queryString1));
            }

            if (debugModeIsOn()) {
                printSparqlDebugInformations(
                        dClassName("gov.nist.ssd.swqb.core.Display"),
                        dMethodName("getPageContent"),
                        dRequestGoal("search link between nodes"),
                        dSparqlText(queryString2));
            }


            Query query1 = QueryFactory.create(queryString1);
            QueryExecution qexec1 = QueryExecutionFactory.create(query1, model);
            ResultSet resultset1 = qexec1.execSelect();

            while (resultset1.hasNext()) {
                QuerySolution next = resultset1.next();

                RDFNode yn = next.get("y");
                RDFNode zn = next.get("z");
                Statement statement = new Statement(n, yn, zn);
                if (!ret.contains(statement)) {
                    ret.add(statement);
                }
            }

            qexec1.close();



            Query query2 = QueryFactory.create(queryString2);
            QueryExecution qexec2 = QueryExecutionFactory.create(query2, model);
            ResultSet resultset2 = qexec2.execSelect();

            while (resultset2.hasNext()) {
                QuerySolution next = resultset2.next();

                RDFNode yn = next.get("y");
                RDFNode zn = next.get("z");
                Statement statement = new Statement(zn, yn, n);
                if (!ret.contains(statement)) {
                    ret.add(statement);
                }
            }

            qexec2.close();

        }

        return ret;
    }

    private static String getDisjonctiveFilterClause(List<RDFNode> rdfNodeList) {
        StringBuilder sb = new StringBuilder();

        Iterator<RDFNode> it = rdfNodeList.iterator();

        if (it.hasNext()) {
            sb.append("?z=" + RDFNodeAsSparqlArg(it.next()) + "\n");
        }

        while (it.hasNext()) {
            sb.append("|| ?z=" + RDFNodeAsSparqlArg(it.next()) + "\n");
        }

        return sb.toString();
    }

    private static String getPageContentSparqlRequest1(String prefixes, String from, String toClause) {
        StringBuilder sb = new StringBuilder();

        sb.append(prefixes).append('\n');
        sb.append("SELECT ?y ?z \n");
        sb.append("  WHERE {\n");
        sb.append("  ").append(from).append(" ?y ?z . \n");
        sb.append("  FILTER ( \n");
        sb.append(toClause);
        sb.append("  ) \n");
        sb.append("  } \n");

        return sb.toString();
    }

    private static String getPageContentSparqlRequest2(String prefixes, String from, String toClause) {
        StringBuilder sb = new StringBuilder();

        sb.append(prefixes).append('\n');
        sb.append("SELECT ?y ?z \n");
        sb.append("  WHERE {\n");
        sb.append("  ?z ?y ").append(from).append(" . \n");
        sb.append("  FILTER ( \n");
        sb.append(toClause);
        sb.append("  ) \n");
        sb.append("  } \n");

        return sb.toString();
    }

    /*
     * Generate the list of nodes in the current page
     */
    protected List<RDFNode> getNodePageList() {
        List<RDFNode> ret = new ArrayList<RDFNode>();
        ret.add(this.currentNode);

        /* The request
         * We can't use count because we need to distinct differents ?x and ?z
         */
        String r = Display.RDFNodeAsSparqlArg(currentNode);
        String queryString =
                prefixManager.getPrefixsTurtleFormat()
                + "SELECT DISTINCT ?x\n"
                + "WHERE {\n"
                + "     {\n"
                + "         " + r + " ?y ?x .\n"
                + "     }\n"
                + "     UNION\n"
                + "     {\n"
                + "         ?x ?y " + r + " .\n"
                + "     }\n"
                + "}\n"
                 + "LIMIT " + this.displayNumber + " OFFSET " + (this.displayNumber * this.currentPage);

        if (debugModeIsOn()) {
            printSparqlDebugInformations(
                    dClassName("gov.nist.ssd.swqb.core.Display"),
                    dMethodName("getNodePageList"),
                    dRequestGoal("generate the list of nodes in the current page"),
                    dSparqlText(queryString));
        }


        Query query = QueryFactory.create(queryString, Syntax.syntaxARQ);
        //Execute the query
        QueryExecution qexec = QueryExecutionFactory.create(query, model);
        ResultSet resultset = qexec.execSelect();

        while (resultset.hasNext()) {
            QuerySolution next = resultset.next();
            RDFNode x = next.get("x");
            ret.add(x);
        }

        return ret;
    }

    private static String getUri(Resource sub) {
        String uri = sub.getURI();
        String nameSpace = sub.getNameSpace();
        Map<String, String> table = prefixManager.getTable();
        if (table.containsKey(nameSpace)) {
            uri = table.get(nameSpace) + ":" + sub.getLocalName();
            if(uri != null)
                return uri;
            else
                return sub.toString();
        }
        return "<" + uri + ">";
    }

    private static String getUri(Literal sub) {
        return sub.toString();
    }

    public static String getUri(RDFNode n) {
        if(n.isAnon())
            return n.toString();
        else if (n.isURIResource()) {
            return Display.getUri(n.asResource());
        } else if (n.isLiteral()) {
            return Display.getUri(n.asLiteral());
        }
        return n.toString();
    }

    /**
     * add < >, <_: > or " " to a node's uri depending on its type
     * @param n
     * @return 
     */
    public static String RDFNodeAsSparqlArg(RDFNode n) {
        String r = getUri(n);
        
        if (n.isAnon()) {
            r = "<_:" + r + ">";
        } else if (n.isLiteral()) {
            r = "\""+n.asLiteral().getLexicalForm().toString()+"\"";
            String dataType = n.asLiteral().getDatatypeURI();
            if(dataType != null)
                r += "^^<"+dataType+">";
        }
        return r;
    }

    public Model getModel() {
        return this.model;
    }

    public void addSetFocusListener(SetFocusListener l) {
        this.listeners.add(l);
    }

    public void removeSetFocusListenr(SetFocusListener l) {
        this.listeners.remove(l);
    }

    public void addChangeDisplayCountListener(ChangeDisplayCountListener l) {
        this.listeners.add(l);
    }

    public void removeChangeDisplayCountListener(ChangeDisplayCountListener l) {
        this.listeners.remove(l);
    }

    public void addChangeIndexListener(ChangeIndexListener l) {
        this.listeners.add(l);
    }

    public void removeChangeIndexListener(ChangeIndexListener l) {
        this.listeners.remove(l);
    }
}
